/*
 * TCSS 558
 * Distributed HashMap
 * Fall 2013
 * James Marquardt
 * Matt Adams
 * Justin Wong
 */
package model;
import static com.google.common.base.Preconditions.*;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import tools.CheckArgument;
import tools.Log;

import node.Node;

/**
 * @author Matt Adams
 */
public class NodeReferences {

    /**
     * Node's finger table.
     */
    private FingerTable fingerTable = null;

    /**
     * Node's successor.
     */
    private Node successor = null;

    /**
     * Node's predecessor.
     */
    private Node predecessor = null;

    /**
     * Node's local ID.
     */
    private int localID;
    
    /**
     * This node's host and port info.
     */
    private HostPort localHostPort = null;
    
    /**
     * Max number of nodes allowed in this chord.
     */
    private final int mValue;
    
    private final Entries entries;

    public NodeReferences(final int id, final HostPort hostPort, final Entries entries, final int mValue) {
        this.localID = id;
        this.localHostPort = hostPort;
        this.entries = entries;
        this.mValue = mValue;
        this.fingerTable = new FingerTable(this.localID, this);
    }
    
    public final synchronized void setPredecessor(Node node){
        checkNotNull(node, "Cannot set null predecessor.");
    	this.predecessor = node;
    	messageToFile("Predecessor set to node " + node.getNodeID());
    }
    
    public final synchronized void setSuccessor(Node node) {
    	this.successor = node;
    	if (node != null) {
    	    this.fingerTable.addReference(node);
    	    messageToFile("Successor set to node " + node.getNodeID());
    	}
    }
    
    public final synchronized Node getPredecessor() {
        return this.predecessor;
    }
    
    public final synchronized Node getSuccessor() {
        return this.successor;
    }
    
    /**
     * Determines the closest preceding node for the given ID based on finger
     * table, successor, and predecessor.
     * 
     * @param key ID to find closest preceding node for.
     * @return Reference on closest preceding node.
     */
    public final synchronized Node getClosestPrecedingNode(final int key) {
        checkArgument(key >= 0 || key < Math.pow(2, this.mValue) - 1, 
                "Index must be greater than 0 and less than " + ((int) Math.pow(2, this.mValue) - 1));
        Node closestNode = null;
        Map<Integer, Node> possibleNodes = new HashMap<Integer, Node>();
        // Gets closest preceding reference from finger table
        Node closestNodeFromFingerTable = this.fingerTable.getClosestPrecedingNode(key);
        if (closestNodeFromFingerTable != null) {
            possibleNodes.put(closestNodeFromFingerTable.getNodeID(), closestNodeFromFingerTable);
        }
        //Adds successor if not null
        Node successor = this.successor;
        if (successor != null) {
            possibleNodes.put(successor.getNodeID(), successor);
        }
        //Add predecessor if its ID precedes the key.
        if (this.predecessor != null && CheckArgument.isInInterval(key, this.predecessor.getNodeID(), this.localID)) {
            //predecessorIfAppropriate = this.predecessor;
            possibleNodes.put(this.predecessor.getNodeID(), predecessor);
        }
        final List<Integer> sortedListOfIDs = new ArrayList<Integer>(possibleNodes.keySet());
        sortedListOfIDs.add(key);
        final int sortedListSize = sortedListOfIDs.size();
       
        //Sort list of IDs
        Collections.sort(sortedListOfIDs);
        
        //Want to find the ID one index lower than the key value.
        final int keyIndex = sortedListOfIDs.indexOf(key);

        //Treats the IDs in the list as a circular array.
        final int index = (sortedListSize + (keyIndex - 1)) % sortedListSize;
 
        final int closestNodeID = sortedListOfIDs.get(index);
        closestNode = possibleNodes.get(closestNodeID);

        checkNotNull(closestNode, "closestNode must not be null!");
        Log.out("Found closest preceding node. Local node ID: " + this.localID + ", Preceding node ID: " + closestNode.getNodeID());
        return closestNode;
    }
    
    /**
     * Adds the given node reference to the finger table and successor, if
     * appropriate.
     * 
     * @param newReference Reference to be added to the local data structures.
     */
    public final synchronized void addReference(Node newReference) {
        checkNotNull(newReference, "Node reference being added cannot be null.");

        this.fingerTable.addReference(newReference);
        if (this.successor == null || !this.successor.equals(this.fingerTable.getFinger(0))) {
            this.successor = this.fingerTable.getFinger(0);
            Log.out("Set new successor. Local node ID: " + this.localID + ", Successor ID: " + this.successor.getNodeID());
            messageToFile("Successor set to node " + this.successor.getNodeID() );
        }
    }
    
    /**
     * Adds the given node reference to the finger table and successor AND
     * sets it as new predecessor reference, if appropriate. Appropriateness is
     * given if either the former predecessor reference was null
     * or the new reference's ID is located between the old predecessor ID and
     * this node's ID. Even if not appropriate as predecessor, the reference is
     * added to finger table and successor.
     * 
     * @param potentialPredecessor Reference which should be this node's predecessor.
     */
    public final synchronized boolean addReferenceAndUpdatePredecessor(Node potentialPredecessor) {
        boolean result = false;
        checkNotNull(potentialPredecessor, "Reference to potential predecessor cannot be null.");
        if (this.predecessor != null) {
        Log.out("Add Reference as PREDECESSOR. Interval: check(" + potentialPredecessor.getNodeID() + ") interval(" + this.predecessor.getNodeID() + ", " + this.localID + ")");
        }
        if (this.predecessor == null || CheckArgument.isInInterval(potentialPredecessor.getNodeID(), 
                                                                   this.predecessor.getNodeID(), 
                                                                   this.localID)) {
            this.setPredecessor(potentialPredecessor);
            result = true;
            Log.out("Set Predecessor to nodeID: " + potentialPredecessor.getNodeID());
        } else {
            Log.out("Predecessor not added, nodeID: " + potentialPredecessor.getNodeID());
        }
        // add reference to finger table and successor.
        this.addReference(potentialPredecessor);
        return result;
    }
    
    /**
     * Removes the given node reference from the finger table and resets the successor. 
     * If the given reference is the current predecessor, the predecessor
     * reference will be null afterwards.
     * 
     * @param oldReference Reference to remove from ALL data structures.
     */
    public final synchronized void removeReference(final Node oldReference) {
        checkNotNull(oldReference, "Reference to be removed cannot be null.");
        try {
            Log.out("Attempting to REMOVE reference node: " + oldReference.getNodeID() + " from node: " + this.localID);
            this.fingerTable.removeReference(oldReference);
            messageToFile("Reference to node " + oldReference.getNodeID() + " removed.");
            System.out.println(this.fingerTable.toString());
            this.successor = this.fingerTable.getFinger(0);

            if (oldReference.equals(this.predecessor)) {
                this.predecessor = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * Determines if the given reference is contained in any one data structure,
     * ie. finger table, successor, or predecessor reference.
     * 
     * @param newReference Reference to look up.
     * @return true, if the reference is contained and false if not.
     */
    public final synchronized boolean containsReference(Node newReference) {
        checkNotNull(newReference, "Reference to be checked cannot be null.");
        boolean result = false;
        result = (this.fingerTable.containsReference(newReference) || 
                newReference.equals(this.successor) || 
                newReference.equals(this.predecessor));
        return result;
    }
    
    /**
     * @return Returns M value for Chord.
     */
    public int getMValue() {
        return this.mValue;
    }
    
    /**
     * @return Returns this node's finger table.
     */
    public FingerTable getFingerTable(){
    	return this.fingerTable;
    }
    
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("Node ID: " + this.localID);
        if (this.successor != null) {
        sb.append("\nsuccessor: " + this.successor.getNodeID());
        }
        if (this.predecessor != null) {
        sb.append("\npredecessor: " + this.predecessor.getNodeID());
        }
        sb.append("\n");
        return sb.toString();
    }
    
    /**
     * Prints message to log file.
     * 
     * @param the_message
     */
    private void messageToFile(String the_message) {
    	try {
    	    Log.toFile(the_message, this.localID);
    	} catch (FileNotFoundException e) {
    		Log.err("could not create or find log file for node " + this.localID);
    	}
    }
    
    public HostPort getHostPort() {
        return this.localHostPort;
    }
    
    public Entries getEntriesFromReferences() {
        return this.entries;
    }
    
}
